#!/usr/bin/perl -w
#
# cpandeps.pl - Create include files for makefiles to set CPAN dependencies
#
# See the POD documentation at the end of this file for more details.

use strict;
use warnings;

use Carp;
use Getopt::Long;
use Pod::Usage;
use File::Basename;
use Data::Dumper;

my $SPECDIR = 'spec';

my $configfile = 'cpanmods.cfg';
my $specfile   = '';
my $spec2 = 0;
my $help       = 0;
my $debug = 0;
my $outfile;

GetOptions(
    "help"     => \$help,
    "config=s" => \$configfile,
    "out=s"    => \$outfile,
    "spec=s"   => \$specfile,
    "spec2"     => \$spec2,
    "debug"     => \$debug,
) or pod2usage( -exitval => 1, -verbose => 0 );

if ($help) {
    pod2usage( -exitval => 1, -verbose => 2 );
}

#if ( not $outfile ) {
#    warn "Error: --out not specified\n";
#    pod2usage(-exitval => 1, -verbose => 0);
#}

our $modlist = {};

do $configfile or die "Error reading '$configfile': $@";

my $OUT;

if ($outfile) {
    if ( not open $OUT, '>', $outfile ) {
        die "Error opening $outfile: $!";
    }
}
else {
    $OUT = *STDOUT;
}

sub name2pkgname {
    my $name = shift;
    $name =~ s/::/-/g;
    return $name;
}

# create lookup table for getting the classname for a module name
my $mod2class = { map { my $c = $_; s/::/-/g; ($_, $c) } keys %{ $modlist } };

if ($debug) {
    my $mods=0;
    foreach my $mod ( keys %{ $modlist } ) {
        $mods++ if not $modlist->{$mod}->{ignore};
    }
    warn "CPAN modules to be fetched: $mods\n";
    exit;
#    die "DEBUG: mod2class=", Dumper($mod2class);
}

# name2classname(NAME) - returns IO::Prompt for IO-Prompt
sub name2classname {
    my $name = shift;
    return $mod2class->{$name};
#        if ( $name eq 'Git-PurePerl' ) {
#            warn "name2classname($name) classes=", join(', ', keys %{ $modlist });
#        }
#    foreach my $classname ( keys %{$modlist} ) {
#        my $newname = $classname;
#        $newname =~ s/::/-/g;
#        if ( $name eq $newname ) {
#            return $classname;
#        }
#    }
#    return $name;
}

# rpmqspec(SPEC) - query rpm specfile returning a reference to a named list with
# the keys VERSION, RELEASE and ARCH.
sub rpmqspec {
    my $spec = shift;
    my $ret  = {};
    if ( not -f $spec ) {
        confess "Error: $spec not found";
    }

    my $cmd = "rpm -q --queryformat '"
      . join( "\n",
        map { $_ . ' %{' . $_ . '}' } qw( NAME VERSION RELEASE ARCH ) )
      . "' --specfile $spec";
    my $RPM;
    if ( not open $RPM, '-|', $cmd ) {
        die "Error running $cmd: $!";
    }

    while ( my $line = <$RPM> ) {
        chomp $line;

        #        warn "Processing line: '$line'";
        my ( $key, $val ) = split( /\s+/, $line, 2 );
        $ret->{$key} = $val;
    }
    return $ret;
}

# rpmeval(EVAL) - run rpm eval for the given string
sub rpmeval {
    my $eval = shift;
    my $ret  = `rpm --eval '$eval'`;
    chomp $ret;
    return $ret;
}

#
# Start printing the output...
#

print $OUT "# Generated by $0 ", join( ' ', @ARGV ), "\n\n";

#
# If the --spec option was not used, just generate the list of modules
#

#if ( $debug ) {
#    die "DEBUG: Git-PurePerl => ", name2classname('Git-PurePerl');
#}

if ( not $specfile ) {
    my ( @CPANFETCH, @CPANSPECS );

    foreach my $modname ( keys %{$modlist} ) {
        my $rec = $modlist->{$modname};
        my $pkgname = $rec->{pkgname} || ( 'perl-' . name2pkgname($modname) );

#        if ( exists $rec->{provides} and ref( $rec->{provides} ) eq 'ARRAY' ) {
#            push @CPANNOBUILD, map { s/::/-/g; $_ } @{ $rec->{provides} };
#        }
        if ( not $rec->{ignore} ) {
            push @CPANFETCH, $rec->{searchname} || $modname;
            push @CPANSPECS, "$SPECDIR/$pkgname.spec";

 #            if ( exists $rec->{patch} ) {
 #                push @EXCEPTIONS,
 #                  'CPAN.' . $pkgname . '.patch := ' . $SPECDIR . '/' . $rec->{patch};
 #            }
        }
    }

    print $OUT join( " \\\n\t", 'CPANFETCH := ', sort @CPANFETCH ), "\n\n";
    print $OUT join( " \\\n\t", 'CPANSPECS := ', sort @CPANSPECS ), "\n\n";

    #    print join( " \\\n\t", 'CPANNOBUILD := ', sort @CPANNOBUILD ), "\n\n";
    #    print join( "\n", @EXCEPTIONS ), "\n\n";
}

#
# If the --spec2 option was used, generate the .spec2 based on the .spec file.
#
elsif ( $specfile and $spec2 ) {
    my $rpmsrcdir   = rpmeval('%{_sourcedir}');
    my $specinfo    = rpmqspec($specfile);
    my $base        = $specinfo->{NAME};
    my $modname     = basename($base);
    $modname =~ s{\A perl- }{}xms;
    my $classname = name2classname($modname);

    my $SPECFH;
    if ( not open ( $SPECFH, '<', $specfile ) ) {
        die "Error opening $specfile for reading: $!";
    }

    my $nobuild = {};
    # Add any explicity overrides for this module, if found
    if ( exists $modlist->{$classname}->{norequire} and ref($modlist->{$classname}->{norequire}) eq 'ARRAY' ) {
        foreach my $noreq ( @{ $modlist->{$classname}->{norequire} } ) {
            $nobuild->{$noreq}++;
        }
    }
    while ( my $line = <$SPECFH> ) {
        next if $line =~ m/^BuildRequires:/;

        if ( $line =~ /^Requires:\s+perl\(([^)]+)\)/ ) {
            next if $nobuild->{$1};
        }
        print $OUT $line;
    }
}

#
# If the --spec option was used, generate the dependency file for that
# spec file.
#
else {
    my $rpmsrcdir   = rpmeval('%{_sourcedir}');
    my $rpmbuilddir = rpmeval('%{_builddir}');
    my $specinfo    = rpmqspec($specfile);
    my $base        = $specinfo->{NAME};
    my $modver         = $specinfo->{VERSION};
    my $modname     = basename($base);
    $modname =~ s{\A perl- }{}xms;
    my $classname = name2classname($modname);

    my $rpmpkg =
        rpmeval('%{_rpmdir}') . '/'
      . $specinfo->{ARCH}
      . '/perl-'
      . $modname . '-'
      . $specinfo->{VERSION} . '-'
      . $specinfo->{RELEASE} . '.'
      . $specinfo->{ARCH} . '.rpm';
    my $src;
    my $tgz;
    my $SPEC;

    my @prereqs = ();

#    print $OUT 'PERLINC := $(PERLINC):', '../', $modname, '-', $modver,
#      '/blib/lib', "\n";
#    print $OUT 'PERLINC := $(PERLINC):', '../', $modname, '-', $modver,
#      '/blib/arch', "\n";
    print $OUT 'PERLINC := $(PERLINC):', $rpmbuilddir, '/', $modname, '-', $modver,
      '/blib/lib', "\n";
    print $OUT 'PERLINC := $(PERLINC):', $rpmbuilddir, '/', $modname, '-', $modver,
      '/blib/arch', "\n";
    print $OUT 'CPANRPMS += ', $rpmpkg, "\n\n";

    # Open the spec file
    if ( not open $SPEC, '<', $specfile ) {
        die "Error opening $specfile: $!";
    }

    #
    # We need a lookup table so when a sub-module of a package is listed
    # as a 'Requires' that the parent module name is used.
    my $provides = {};
    my $nobuild  = {};
    foreach my $modname ( keys %{$modlist} ) {
        my $rec = $modlist->{$modname};
        if ( exists $rec->{provides} and ref( $rec->{provides} ) eq 'ARRAY' ) {
            foreach my $child ( @{ $rec->{provides} } ) {
                $provides->{$child} = $modname if not $rec->{ignore};
                $nobuild->{$child} = $modname;
            }
        }
    }
    # Add any explicity overrides for this module, if found
    if ( exists $modlist->{$classname}->{norequire} and ref($modlist->{$classname}->{norequire}) eq 'ARRAY' ) {
        foreach my $noreq ( @{ $modlist->{$classname}->{norequire} } ) {
            $nobuild->{$noreq}++;
        }
    }

    my $depnames = {};    # use hash because we want unique names
    foreach my $ln (<$SPEC>) {

        # While we have it open, grab the Source0 line
        if ( $ln =~ m/^Source0:\s+(\S+)$/ ) {
            $src = $1;
            my $modver = $specinfo->{VERSION};
            $src =~ s/%{version}/$modver/xms;
            $tgz = basename($src);
        }
        elsif ( $ln =~ m{ \A (?:Build)?Requires: \s+ perl\( ([^)]+) \) }xms ) {
            my $dep = $1;

            # ignore nobuild stuff
            next if exists $nobuild->{$dep} or $dep eq $classname;

            $depnames->{ $provides->{$dep} || $dep }++;
        }
    }
    close $SPEC;

    # add any dependencies manually set in config file
    if ( exists $modlist->{$classname} and exists $modlist->{$classname}->{'require'} and ref( $modlist->{$classname}->{'require'} ) eq 'ARRAY' ) {
#        warn "DEBUG: Processing 'requires' for $modname";
        foreach my $dep ( @{ $modlist->{$classname}->{'require'} } ) {
            $depnames->{$dep}++;
        }
    }
    die "STOP AT $modname" if  $modname eq 'Git::PurePerl';

    my $builtdir = $rpmbuilddir . '/' . $modname . '-' . $specinfo->{VERSION} . '/blib/built';

    # Generate list of dependent built directories
    foreach my $dep ( sort keys %{$depnames} ) {
        my $depspec     = $SPECDIR . '/perl-' . name2pkgname($dep) . '.spec';
        my $depinfo     = rpmqspec($depspec);
        my $depbuiltdir = $rpmbuilddir . '/' . name2pkgname($dep) . '-' . $depinfo->{VERSION} . '/blib/built';
        push @prereqs, $depbuiltdir;
#        print $OUT $builtdir, ': ', $depbuiltdir, "\n";
    }

    # Some sanity checks
    if ( not $src ) {
        die "Error: did not find src for $specfile";
    }
    if ( not $tgz ) {
        die "Error: did not find tgz for $specfile";
    }

    #
    # Rule for fetching tarball
    #
    print $OUT "$rpmsrcdir/$tgz: $SPECDIR/$base.spec\n";
    print $OUT "\t", '@wget --no-verbose -O ', $rpmsrcdir, '/', $tgz, ' ', $src,
      "\n";
    print $OUT "\t", '@test -f ', $rpmsrcdir, '/', $tgz, ' && touch ',
      $rpmsrcdir, '/', $tgz, "\n";
    print $OUT "\n";

    #
    # Rule for building package and temporary LIB files
    #
    print $OUT "$rpmbuilddir/$modname-$modver/blib/built $rpmpkg: \\\n";
    print $OUT "$rpmsrcdir/$tgz \\\n";
    print $OUT "$SPECDIR/$base.spec2";
    if ( @prereqs ) {
        print $OUT join(" \\\n", '', @prereqs); 
    }
    print $OUT "\n";
    
    # Commands for building package
    print $OUT "\t",
      '@PERL_LOCAL_LIB_ROOT= PERL_MB_OPT= PERL_MM_OPT= ',
      "rpmbuild -bb $SPECDIR/$base.spec2\n";
    print $OUT "\t", 'touch ', $rpmbuilddir, '/', $modname, '-',
      $specinfo->{VERSION}, '/blib/built', "\n";
    print $OUT "\n";

    print $OUT "$SPECDIR/$base.specdep: $SPECDIR/$base.spec\n";
#    print $OUT 'cpan/', $base, '.spec:', "\n";
    print $OUT "$SPECDIR/$base: $rpmpkg\n";

    if ( not exists $modlist->{$classname} ) {
        die "Module '$classname' ($modname) not found in module list";
    }
    my $patch     = $modlist->{$classname}->{patch};
#    warn "DEBUG: modname=$modname, class=$classname, patch=", ($patch||'<not set>');
    if ($patch) {
        print $OUT "$SPECDIR/$base.spec2: $SPECDIR/$base.spec cpandeps.pl $patch\n";
        print $OUT "\t./cpandeps.pl --config \$(DISTCPANCFG) \\\n",
            "\t\t--spec2 \\\n",
            "\t\t--spec $SPECDIR/$base.spec \\\n",
            "\t\t--out $SPECDIR/$base.spec2\n";
#        print $OUT "\tperl -ne 'print unless m/^BuildRequires/' ",
#          "$SPECDIR/$base.spec > $SPECDIR/$base.spec2\n";
        print $OUT "\tpatch $SPECDIR/$base.spec2 < $patch\n";
    }
    else {
        print $OUT "$SPECDIR/$base.spec2: $SPECDIR/$base.spec cpandeps.pl\n";
        print $OUT "\t./cpandeps.pl --config \$(DISTCPANCFG) \\\n",
            "\t\t--spec2 \\\n",
            "\t\t--spec $SPECDIR/$base.spec \\\n",
            "\t\t--out $SPECDIR/$base.spec2\n";
#        print $OUT "\tperl -ne 'print unless m/^BuildRequires/' ",
#          "$SPECDIR/$base.spec > $SPECDIR/$base.spec2\n";
    }
    print $OUT "\n";

}

__END__

=head1 NAME

cpandeps.pl - Create include files for makefiles to set CPAN dependencies

=head1 SYNOPSIS

cpandeps.pl [options]

Options:

  --help                This help message
  --config CONFIG       Name of configuration file
  --spec SPECFILE       Name of specfile [optional]
  --out OUTFILE         Name of output file [optional]

=head1 OPTIONS

=over 8

=item B<--help>

Print the help message and exit.

=item B<--config> FILENAME

The name of the configuration file. This file is in Perl syntax format and 
assigns an anonymous hash to the C<$modlist> variable. The structure looks like
the following:

  $modlist = {
    MODNAME1 => {
        key1 => val1,
        key2 => val2,
    },
    ...
  };

=item B<--spec> FILENAME

The name of the spec file to process. If the specfile is given, that file is
parsed for dependencies and, together with the configuration file, a dependency
file that is pulled in as an include file by the makefile is generated.

When not specified, a dependency file with a list of all modules is created.

=item B<--out> FILENAME

The name of the output file to generate.

=item B<--spec2>

Generates the .spec2 file based on the input from the given .spec file.

=back

=head1 DESCRIPTION

This utility helps automate the build process of CPAN modules by generating
include files for "make" that contain targets and dependencies that describe
the relationships between the needed CPAN modules. The configuration file
defines exactly which CPAN modules are to be built. 

The automated CPAN package build process is a bit bumpy, so the config file
also contains any special cases like patch files or unresolved dependencies.

When run without the --spec option, a single list of all modules is
created. This is used in early runs of "make" to fetch the spec files from
CPAN and as a list of include files for each module.

After the spec files have been created (not a job of this script, by the way),
using the "--spec" option causes the given spec file to be scanned for
dependencies and the resulting .specdep file is created.

=cut
